{
  "name": "Case 72 - Supply Chain Analyst Vendor Facility Due Diligence",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 9
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -976,
        -192
      ],
      "id": "edd5771e-4d84-4895-8575-29e8b45b8af6",
      "name": "Weekly Schedule - Monday 9 AM"
    },
    {
      "parameters": {
        "actorId": {
          "__rl": true,
          "value": "Xb8osYTtOjlsgI6k9",
          "mode": "id",
          "cachedResultName": "Google Maps Scraper"
        },
        "customBody": "{\n  \"language\": \"en\",\n  \"maxReviews\": 100,\n  \"personalData\": true,\n  \"startUrls\": [\n    {\n      \"url\": \"https://www.google.com/maps/place/FedEx+Authorized+Ship+Center/@40.7477973,-73.9502966,17z/data=!3m1!5s0x89c259287f0cfe49:0xb4761fbef9db1b83!4m16!1m9!3m8!1s0x89c259354b8e7115:0xe574e37954cad8a3!2sFedEx+Authorized+Ship+Center!8m2!3d40.7477933!4d-73.9477217!9m1!1b1!16s%2Fg%2F11fhqnsh2t!3m5!1s0x89c259354b8e7115:0xe574e37954cad8a3!8m2!3d40.7477933!4d-73.9477217!16s%2Fg%2F11fhqnsh2t?entry=ttu&g_ep=EgoyMDI1MTIwOS4wIKXMDSoASAFQAw%3D%3D\"\n    }\n  ],\n  \"reviewsSort\": \"newest\",\n  \"reviewsOrigin\": \"all\"\n}"
      },
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 1,
      "position": [
        -784,
        -192
      ],
      "id": "7e85b5a2-aaf0-4263-9b33-11e5392f4132",
      "name": "Scrape Vendor Facility Reviews",
      "credentials": {
        "apifyApi": {
          "id": "w5S6YBbbyUddEfQA",
          "name": "Apify account"
        }
      }
    },
    {
      "parameters": {
        "resource": "Datasets",
        "datasetId": "={{ $json.defaultDatasetId }}",
        "limit": 100
      },
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 1,
      "position": [
        -592,
        -192
      ],
      "id": "3137641c-ea49-44d7-99d8-d2bce78bfa33",
      "name": "Get Dataset Items",
      "credentials": {
        "apifyApi": {
          "id": "w5S6YBbbyUddEfQA",
          "name": "Apify account"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "id": "filter-1star",
              "leftValue": "={{ $json.stars }}",
              "rightValue": 1,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            },
            {
              "id": "filter-2star",
              "leftValue": "={{ $json.stars }}",
              "rightValue": 2,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2,
      "position": [
        -400,
        -192
      ],
      "id": "855dcb6d-5b0b-477f-b764-e6286389dc19",
      "name": "Filter 1-2 Star Reviews"
    },
    {
      "parameters": {
        "jsCode": "// Filter out reviews without meaningful text\nconst items = $input.all();\nconst validItems = [];\n\nitems.forEach((item, index) => {\n  const text = item.json.text || item.json.textTranslated || '';\n  \n  // Skip if text is empty or too short\n  if (text.trim().length < 10) {\n    console.log(`Skipping item ${index}: Empty or too short review text`);\n    return;\n  }\n  \n  validItems.push(item);\n});\n\nconsole.log(`Filtered: ${items.length} total → ${validItems.length} valid reviews with text`);\n\nreturn validItems;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -208,
        -192
      ],
      "id": "ff0cc1a2-4f27-4615-8615-64d3826d6f09",
      "name": "Validate Review Text"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-4o",
          "mode": "list",
          "cachedResultName": "GPT-4o"
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "You are a Supply Chain Risk Analyst. Analyze reviews from delivery drivers, employees, or visitors about vendor facilities. Identify operational red flags.\n\nReturn ONLY this exact JSON format with NO other text:\n\n{\n  \"risk_category\": \"Delivery Reliability|Quality Control Issues|Staffing Concerns|Technology Adoption|Peak Season Performance|Communication Quality|Other\",\n  \"risk_level\": \"critical|high|medium|low\",\n  \"reviewer_type\": \"Delivery Driver|Employee|Customer|Visitor|Unknown\",\n  \"operational_issue\": \"specific problem identified\",\n  \"key_quote\": \"most important sentence from review\"\n}\n\nCATEGORIES:\n\nDelivery Reliability = Loading dock delays, scheduling issues, missed pickups\nKeywords: wait at dock, loading took hours, appointment not honored, truck delays\nExample: \"Waited 2 hours at loading dock\" → Delivery Reliability\n\nQuality Control Issues = Damaged goods, wrong items, poor packaging, inspection failures\nKeywords: damaged, wrong product, poor packaging, quality issues, defective\nExample: \"Received damaged pallets frequently\" → Quality Control Issues\n\nStaffing Concerns = Understaffed, high turnover, untrained workers, poor management\nKeywords: understaffed, no one available, workers didn't know, management issues\nExample: \"Facility seems understaffed\" → Staffing Concerns\n\nTechnology Adoption = Outdated systems, no tracking, manual processes, poor integration\nKeywords: no barcode, manual paperwork, outdated system, can't track\nExample: \"Still using paper manifests\" → Technology Adoption\n\nPeak Season Performance = Holiday delays, unable to scale, capacity issues\nKeywords: holiday chaos, couldn't handle volume, December nightmare, peak season fail\nExample: \"Complete chaos during holiday season\" → Peak Season Performance\n\nCommunication Quality = Poor responsiveness, no updates, language barriers, unclear instructions\nKeywords: no one answers, couldn't reach anyone, confusing instructions, poor communication\nExample: \"No one responds to emails\" → Communication Quality\n\nOther = Everything else\n\nRISK LEVEL:\n- critical: Major operational failures, safety issues, contract-breaker\n- high: Serious recurring problems, significant delays/damages\n- medium: Frustrating issues, inefficiencies\n- low: Minor inconveniences, isolated incidents\n\nREVIEWER TYPE:\n- Delivery Driver: Mentions trucks, loading, dock, pickup, delivery\n- Employee: Mentions working there, management, coworkers, shifts\n- Customer: Mentions ordering, receiving goods, purchasing\n- Visitor: Mentions tour, meeting, appointment\n- Unknown: Cannot determine\n\nReturn ONLY the JSON. No explanation. Start with { and end with }"
            },
            {
              "content": "=Analyze this vendor facility review:\n\nFacility: {{ $json.title }}\nReviewer: {{ $json.name }}\nRating: {{ $json.stars }} stars\nDate: {{ $json.publishedAtDate }}\nReview: {{ $json.text }}\n\nReturn only JSON."
            }
          ]
        },
        "builtInTools": {},
        "options": {
          "maxTokens": 500,
          "temperature": 0
        }
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.1,
      "position": [
        -16,
        -192
      ],
      "id": "8ab4c515-b084-49d5-bbf8-9c250c751880",
      "name": "AI: Analyze Vendor Risk",
      "credentials": {
        "openAiApi": {
          "id": "ICwxUBbatsF2sDvy",
          "name": "OpenAi account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// ENHANCED PARSER WITH VALIDATION\nconst items = [];\nconst input = $input.all();\n\nfunction extractJSON(text) {\n  const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n  if (!jsonMatch) return null;\n  return jsonMatch[0];\n}\n\ninput.forEach((item, index) => {\n  try {\n    let aiText = item.json.output[0].content[0].text || '';\n    \n    // Clean markdown and extra text\n    aiText = aiText\n      .replace(/```json/gi, '')\n      .replace(/```/g, '')\n      .trim();\n    \n    // Extract JSON\n    const jsonStr = extractJSON(aiText);\n    \n    if (!jsonStr) {\n      throw new Error('No JSON found in response');\n    }\n    \n    // Parse\n    const parsed = JSON.parse(jsonStr);\n    \n    // Validate required fields\n    if (!parsed.risk_category || !parsed.risk_level || !parsed.reviewer_type) {\n      throw new Error('Missing required fields');\n    }\n    \n    items.push({ json: parsed });\n    \n  } catch (error) {\n    console.error(`Parse error for item ${index}:`, error.message);\n    \n    items.push({\n      json: {\n        risk_category: 'Other',\n        risk_level: 'low',\n        reviewer_type: 'Unknown',\n        operational_issue: 'Parse error',\n        key_quote: ''\n      }\n    });\n  }\n});\n\nreturn items;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        288,
        -192
      ],
      "id": "bd3b94fb-d920-44c9-812d-1bddc7519ed9",
      "name": "Parse AI Response"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "cat",
              "name": "risk_category",
              "value": "={{ $json.risk_category }}",
              "type": "string"
            },
            {
              "id": "level",
              "name": "risk_level",
              "value": "={{ $json.risk_level }}",
              "type": "string"
            },
            {
              "id": "type",
              "name": "reviewer_type",
              "value": "={{ $json.reviewer_type }}",
              "type": "string"
            },
            {
              "id": "issue",
              "name": "operational_issue",
              "value": "={{ $json.operational_issue && $json.operational_issue.includes('Parse error') ? 'Not sufficient data available' : $json.operational_issue }}",
              "type": "string"
            },
            {
              "id": "quote",
              "name": "key_quote",
              "value": "={{ $json.key_quote && $json.key_quote !== '' ? $json.key_quote : 'No quote available' }}",
              "type": "string"
            },
            {
              "id": "date",
              "name": "review_date",
              "value": "={{ $('Filter 1-2 Star Reviews').item.json.publishedAtDate }}",
              "type": "string"
            },
            {
              "id": "reviewer",
              "name": "reviewer_name",
              "value": "={{ $('Filter 1-2 Star Reviews').item.json.name }}",
              "type": "string"
            },
            {
              "id": "rating",
              "name": "star_rating",
              "value": "={{ $('Filter 1-2 Star Reviews').item.json.stars }}",
              "type": "number"
            },
            {
              "id": "text",
              "name": "review_text",
              "value": "={{ $('Filter 1-2 Star Reviews').item.json.textTranslated || $('Filter 1-2 Star Reviews').item.json.text }}",
              "type": "string"
            },
            {
              "id": "url",
              "name": "review_url",
              "value": "={{ $('Filter 1-2 Star Reviews').item.json.reviewUrl }}",
              "type": "string"
            },
            {
              "id": "facility",
              "name": "facility_name",
              "value": "={{ $('Filter 1-2 Star Reviews').item.json.address }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        464,
        -192
      ],
      "id": "d40facdb-5be3-400b-9779-bed090808180",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "1pEwGnA74o5kTy2MVMB4VbG5EsZlxN9jJR6Pg2nXJgWk",
          "mode": "list",
          "cachedResultName": "Case 72 - Supply chain sheet headers",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1pEwGnA74o5kTy2MVMB4VbG5EsZlxN9jJR6Pg2nXJgWk/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "Sheet1",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1pEwGnA74o5kTy2MVMB4VbG5EsZlxN9jJR6Pg2nXJgWk/edit#gid=0"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Review_Date": "={{ $json.review_date }}",
            "Reviewer_Name": "={{ $json.reviewer_name }}",
            "Reviewer_Type": "={{ $json.reviewer_type }}",
            "Star_Rating": "={{ $json.star_rating }}",
            "Review_Text": "={{ $json.review_text }}",
            "Risk_Category": "={{ $json.risk_category }}",
            "Risk_Level": "={{ $json.risk_level }}",
            "Operational_Issue": "={{ $json.operational_issue }}",
            "Key_Quote": "={{ $json.key_quote }}",
            "Review_URL": "={{ $json.review_url }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Review_Date",
              "displayName": "Review_Date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Reviewer_Name",
              "displayName": "Reviewer_Name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Reviewer_Type",
              "displayName": "Reviewer_Type",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Star_Rating",
              "displayName": "Star_Rating",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Review_Text",
              "displayName": "Review_Text",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Risk_Category",
              "displayName": "Risk_Category",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Risk_Level",
              "displayName": "Risk_Level",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Operational_Issue",
              "displayName": "Operational_Issue",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Key_Quote",
              "displayName": "Key_Quote",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Review_URL",
              "displayName": "Review_URL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        656,
        -192
      ],
      "id": "2498cc3d-d793-4fb9-a451-f70204baca5f",
      "name": "Log to Google Sheet",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "LOs2dbk9lby0NfDM",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const items = $('Edit Fields').all();\n\nif (items.length === 0) {\n  return [{ json: { total_risk_signals: 0, risk_breakdown: {}, severity_breakdown: {}, reviewer_type_breakdown: {}, critical_issues: [], facility_name: 'Unknown' } }];\n}\n\nconst riskCount = {};\nconst severityCount = {};\nconst reviewerTypeCount = {};\nconst allIssues = [];\nconst facility_name = items[0].json.facility_name || 'Unknown';\n\nitems.forEach(item => {\n  const data = item.json;\n  const category = data.risk_category || 'Other';\n  riskCount[category] = (riskCount[category] || 0) + 1;\n  const level = data.risk_level || 'low';\n  severityCount[level] = (severityCount[level] || 0) + 1;\n  const type = data.reviewer_type || 'Unknown';\n  reviewerTypeCount[type] = (reviewerTypeCount[type] || 0) + 1;\n  \n  allIssues.push({\n    issue: data.operational_issue || '',\n    quote: data.key_quote || '',\n    reviewer: data.reviewer_name || 'Anonymous',\n    date: data.review_date || '',\n    rating: data.star_rating || 0,\n    url: data.review_url || '',\n    risk_level: data.risk_level || 'low',\n    category: data.risk_category || 'Other',\n    reviewer_type: data.reviewer_type || 'Unknown'\n  });\n});\n\nconst severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };\nallIssues.sort((a, b) => (severityOrder[b.risk_level] || 0) - (severityOrder[a.risk_level] || 0));\nconst criticalIssues = allIssues.filter(i => i.risk_level === 'critical' || i.risk_level === 'high').slice(0, 10);\n\nreturn [{ json: { total_risk_signals: items.length, risk_breakdown: riskCount, severity_breakdown: severityCount, reviewer_type_breakdown: reviewerTypeCount, critical_issues: criticalIssues, facility_name: facility_name } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        848,
        -192
      ],
      "id": "c42de7ae-7131-403b-86dd-07418e777ade",
      "name": "Aggregate Risk Assessment"
    },
    {
      "parameters": {
        "sendTo": "supplychainmanager@company.com",
        "subject": "=Vendor Risk Assessment - {{ $json.facility_name }}",
        "emailType": "text",
        "message": "=VENDOR FACILITY RISK ASSESSMENT REPORT\n==========================================\nFacility: {{ $json.facility_name }}\n\n⚠️ RISK SUMMARY:\nTotal Negative Reviews Analyzed: {{ $json.total_risk_signals }}\n\nRISK CATEGORIES IDENTIFIED:\n{{ Object.keys($json.risk_breakdown).map(cat => cat + ': ' + $json.risk_breakdown[cat] + ' incidents').join('\\n') }}\n\nSEVERITY BREAKDOWN:\n{{ Object.keys($json.severity_breakdown).map(sev => sev.toUpperCase() + ': ' + $json.severity_breakdown[sev]).join('\\n') }}\n\nREVIEWER SOURCES:\n{{ Object.keys($json.reviewer_type_breakdown).map(type => type + ': ' + $json.reviewer_type_breakdown[type] + ' reviews').join('\\n') }}\n\n🚨 CRITICAL & HIGH-RISK ISSUES (Top {{ $json.critical_issues.length }}):\n{{ $json.critical_issues.map((issue, i) => (i+1) + '. [' + issue.risk_level.toUpperCase() + ' RISK] ' + issue.category + '\\n   Source: ' + issue.reviewer_type + '\\n   Issue: ' + issue.issue + '\\n   Quote: \"' + issue.quote + '\"\\n   Reviewer: ' + issue.reviewer + ' | ' + issue.rating + ' stars | ' + issue.date).join('\\n\\n') }}\n\nRECOMMENDED ACTIONS:\n1. {{ $json.severity_breakdown.critical ? 'DO NOT SIGN CONTRACT - Schedule immediate facility audit' : 'Proceed with caution - Additional due diligence recommended' }}\n2. Request references from current clients\n3. Conduct on-site inspection focusing on: {{ Object.keys($json.risk_breakdown).slice(0, 3).join(', ') }}\n4. Negotiate SLAs addressing identified risk areas\n5. {{ $json.risk_breakdown['Peak Season Performance'] ? 'Test capacity during peak season before full commitment' : 'Establish performance metrics and monitoring' }}\n\nDUE DILIGENCE NEXT STEPS:\n- Share this report with procurement team\n- Schedule facility tour with operations manager\n- Request operational metrics (on-time delivery %, defect rate, etc.)\n- Check for alternative vendors in same region\n- If proceeding, negotiate penalty clauses for identified risks\n\nGenerated: {{ new Date().toLocaleDateString() }}\n\n⚠️ This assessment is based on public Google Maps reviews and should be combined with formal vendor audits, references, and financial due diligence.",
        "options": {}
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1040,
        -192
      ],
      "id": "f7e3d9dc-b85e-480a-83d3-9c8b18047383",
      "name": "Send Risk Assessment Email",
      "webhookId": "d779e029-f7e2-44d8-b309-17a7c7be387a",
      "credentials": {
        "gmailOAuth2": {
          "id": "cyqCGWcggZNMcSOv",
          "name": "Gmail account"
        }
      }
    }
  ],
  "pinData": {},
  "connections": {
    "Weekly Schedule - Monday 9 AM": {
      "main": [
        [
          {
            "node": "Scrape Vendor Facility Reviews",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Vendor Facility Reviews": {
      "main": [
        [
          {
            "node": "Get Dataset Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Dataset Items": {
      "main": [
        [
          {
            "node": "Filter 1-2 Star Reviews",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter 1-2 Star Reviews": {
      "main": [
        [
          {
            "node": "Validate Review Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Review Text": {
      "main": [
        [
          {
            "node": "AI: Analyze Vendor Risk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI: Analyze Vendor Risk": {
      "main": [
        [
          {
            "node": "Parse AI Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Response": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Log to Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log to Google Sheet": {
      "main": [
        [
          {
            "node": "Aggregate Risk Assessment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Risk Assessment": {
      "main": [
        [
          {
            "node": "Send Risk Assessment Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "23c1eadb-95be-4594-855c-5b5b76e158e7",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "3a43da28588548e21903e71cf1dc3ddd65c24bf0c62e7e4b77542ffe87ad79c6"
  },
  "id": "Wdtn94N8vMdeZTrS",
  "tags": []
}